/**
* This file is part of Oracle, licensed under the MIT License (MIT).
*
* Copyright (c) 2015 Helion3 http://helion3.com/
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.helion3.oracle;
import java.io.File;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import com.helion3.oracle.commands.LockdownCommand;
import com.helion3.oracle.utils.PlaytimeUtil;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import org.slf4j.Logger;
import org.spongepowered.api.Game;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GameStartedServerEvent;
import org.spongepowered.api.event.game.state.GameStoppingServerEvent;
import org.spongepowered.api.plugin.Plugin;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.api.service.ban.BanService;
import org.spongepowered.api.config.DefaultConfig;
import org.spongepowered.api.text.Text;
import com.google.inject.Inject;
import com.helion3.oracle.bans.OracleBanService;
import com.helion3.oracle.commands.LookupCommand;
import com.helion3.oracle.commands.OracleCommands;
import com.helion3.oracle.commands.PlayedCommand;
import com.helion3.oracle.commands.SeenCommand;
import com.helion3.oracle.listeners.PlayerJoinListener;
import com.helion3.oracle.listeners.PlayerQuitListener;
import com.helion3.oracle.players.PlayerIdentification;
import com.helion3.oracle.players.PluginPlayer;
import com.helion3.oracle.tasks.PlaytimeMonitor;
import com.helion3.oracle.utils.AnnouncementUtil;
import com.helion3.oracle.utils.JoinUtil;
import com.helion3.oracle.utils.ServerUtil;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Plugin(id = "oracle", name = "oracle", version = "2.2")
final public class Oracle {
private static List<UUID> allowedInLockdown = new ArrayList<>();
private static Configuration config;
private static boolean inLockdown;
private static Game game;
private static Logger logger;
private static Oracle plugin;
private static HikariDataSource pool;
private int lastAnnouncement = 0;
public static HashMap<UUID,PluginPlayer> oraclePlayers = new HashMap<UUID,PluginPlayer>();
public static HashMap<Player,Integer> playtimeHours = new HashMap<Player,Integer>();
public static int oracleServer = 0;
private Task disconnectsTask;
@Inject
@DefaultConfig(sharedRoot = false)
private File defaultConfig;
@Inject
@DefaultConfig(sharedRoot = false)
private ConfigurationLoader<CommentedConfigurationNode> configManager;
@Inject
public void setGame(Game injectGame) {
game = injectGame;
}
/**
* Performs bootstrapping of Prism resources/objects.
*
* @param event Server started
*/
@Listener
public void onServerStart(GameStartedServerEvent event) {
plugin = this;
// Load configuration file
config = new Configuration(defaultConfig, configManager);
// init db
pool = initDbPool();
Connection testConn = dbc();
if( pool == null || testConn == null ){
String dbDisabled = "Oracle will disable itself because it couldn't connect to a database.\n";
dbDisabled += "If you're using MySQL, check your config. Be sure MySQL is running.\n";
getLogger().error(dbDisabled);
// @todo disable plugin
return;
}
if(testConn != null){
try {
testConn.close();
} catch (SQLException e) {
logDbError( e );
}
}
// Setup databases
setupDatabase();
// Providers
if (getConfig().getNode("bans", "enabled").getBoolean()) {
game.getServiceManager().setProvider(this, BanService.class, new OracleBanService());
}
// Cache server id
ServerUtil.lookupServer(getConfig().getNode("server-name").getString());
// Cache online players on reload
PlayerIdentification.cacheOnlinePlayerPrimaryKeys();
// Create join records for all currently online players
for (Player pl : getGame().getServer().getOnlinePlayers()){
JoinUtil.registerPlayerJoin( pl, getGame().getServer().getOnlinePlayers().size() );
}
// Register tasks
catchUncaughtDisconnects();
runAnnouncements();
runPlaytimeMonitor();
// Register event listeners
if (Oracle.getConfig().getNode("joins", "enabled").getBoolean()) {
game.getEventManager().registerListeners(this, new PlayerJoinListener(this));
game.getEventManager().registerListeners(this, new PlayerQuitListener(this));
}
// Register commands
game.getCommandManager().register(this, OracleCommands.getCommand(game), "oracle");
game.getCommandManager().register(this, LockdownCommand.getCommand(), "lockdown");
game.getCommandManager().register(this, LookupCommand.getCommand(this), "lookup");
game.getCommandManager().register(this, PlayedCommand.getCommand(this), "played");
game.getCommandManager().register(this, SeenCommand.getCommand(this), "seen");
logger.info("Oracle is listening. Don't worry about the vase.");
}
/**
* Check if a UUID is allowed during a lockdown.
*
* @param uuid
* @return boolean
*/
public static boolean allowedInLockdown(UUID uuid) {
return allowedInLockdown.contains(uuid);
}
/**
* Disable lockdown mode.
*/
public static void disableLockdown() {
inLockdown = false;
allowedInLockdown.clear();
}
/**
* Enable lockdown mode. Rejects connections of
* all players with less than a few hours of playtime.
*/
public static void enableLockdown() {
// Run async so the query doesn't lag the main thread
getGame().getScheduler().createTaskBuilder().async().execute(() -> {
try {
allowedInLockdown = PlaytimeUtil.getTrustedPlayers();
inLockdown = true;
} catch (Exception e) {
inLockdown = true;
e.printStackTrace();
}
}).submit(plugin);
}
/**
* Check if server is locked down.
*
* @return boolean If locked down
*/
public static boolean inLockdown() {
return inLockdown;
}
/**
* Returns the plugin configuration
* @return Configuration
*/
public static Configuration getConfig() {
return config;
}
/**
* Returns the current game
* @return Game
*/
public static Game getGame() {
return game;
}
/**
* Injects the Logger instance for this plugin
* @param log Logger
*/
@Inject
private void setLogger(Logger log) {
logger = log;
}
/**
* Returns the Logger instance for this plugin.
* @return Logger instance
*/
public static Logger getLogger() {
return logger;
}
/**
*
* @return
*/
public HikariDataSource initDbPool() {
String dns = "jdbc:mysql://" +
getConfig().getNode("db", "host").getString() + ":" +
getConfig().getNode("db", "port").getString() + "/" +
getConfig().getNode("db", "name").getString();
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dns);
config.setUsername(getConfig().getNode("db", "user").getString());
config.setPassword(getConfig().getNode("db", "pass").getString());
pool = new HikariDataSource(config);
return pool;
}
/**
*
* @return
* @throws SQLException
*/
public static Connection dbc() {
Connection con = null;
try {
con = pool.getConnection();
} catch (SQLException e) {
System.out.print("Database connection failed. " + e.getMessage());
if (!e.getMessage().contains("Pool empty")) {
e.printStackTrace();
}
}
return con;
}
/**
*
*/
protected void setupDatabase() {
Connection conn = null;
Statement st = null;
try {
conn = dbc();
if (conn == null) return;
String query = "CREATE TABLE IF NOT EXISTS `oracle_announcements` (" +
"`announcement_id` int(11) NOT NULL AUTO_INCREMENT," +
"`announcement` varchar(255) NOT NULL," +
"`type` varchar(16) NOT NULL," +
"`is_active` tinyint(1) NOT NULL," +
"PRIMARY KEY (`announcement_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st = conn.createStatement();
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_bans` (" +
"`ban_id` int(11) NOT NULL AUTO_INCREMENT," +
"`player_id` int(11) unsigned DEFAULT NULL," +
"`ip_id` int(10) unsigned DEFAULT NULL," +
"`staff_player_id` int(11) unsigned NOT NULL," +
"`reason` varchar(255) NOT NULL," +
"`epoch` int(11) unsigned NOT NULL," +
"`unbanned` tinyint(1) NOT NULL DEFAULT '0'," +
"PRIMARY KEY (`ban_id`)," +
"KEY `ip_id` (`ip_id`)," +
"KEY `player_id` (`player_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_ips` (" +
"`ip_id` int(10) unsigned NOT NULL AUTO_INCREMENT," +
"`ip` int(10) unsigned NOT NULL," +
"PRIMARY KEY (`ip_id`)," +
"KEY `ip` (`ip`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_joins` (" +
"`join_id` int(11) unsigned NOT NULL AUTO_INCREMENT," +
"`server_id` int(10) unsigned NOT NULL," +
"`player_count` smallint(4) unsigned NOT NULL," +
"`player_id` int(10) unsigned NOT NULL," +
"`player_join` int(11) NOT NULL," +
"`player_quit` int(11) unsigned DEFAULT NULL," +
"`playtime` int(11) unsigned DEFAULT NULL," +
"`ip_id` int(10) unsigned NOT NULL," +
"PRIMARY KEY (`join_id`)," +
"KEY `player_id` (`player_id`)," +
"KEY `ip_id` (`ip_id`)," +
"KEY `server_id` (`server_id`)," +
"KEY `playtime` (`playtime`)," +
"KEY `player_quit` (`player_quit`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_players` (" +
"`player_id` int(10) unsigned NOT NULL AUTO_INCREMENT," +
"`player` varchar(16) NOT NULL," +
"`player_uuid` binary(16) NOT NULL," +
"PRIMARY KEY (`player_id`)," +
"KEY `player` (`player`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_servers` (" +
"`server_id` int(10) unsigned NOT NULL AUTO_INCREMENT," +
"`server` varchar(16) NOT NULL," +
"PRIMARY KEY (`server_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_unbans` (" +
"`unban_id` int(11) NOT NULL AUTO_INCREMENT," +
"`player_id` int(11) unsigned DEFAULT NULL," +
"`ip_id` int(10) unsigned DEFAULT NULL," +
"`staff_player_id` int(11) unsigned NOT NULL," +
"`epoch` int(11) unsigned NOT NULL," +
"PRIMARY KEY (`unban_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
query = "CREATE TABLE IF NOT EXISTS `oracle_warnings` (" +
"`warning_id` int(11) NOT NULL AUTO_INCREMENT," +
"`player_id` int(11) unsigned NOT NULL," +
"`reason` text NOT NULL," +
"`staff_player_id` int(11) unsigned NOT NULL," +
"`epoch` int(11) unsigned NOT NULL," +
"`deleted` tinyint(1) NOT NULL DEFAULT '0'," +
"PRIMARY KEY (`warning_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=latin1;";
st.executeUpdate(query);
} catch (SQLException e) {
logDbError(e);
} finally {
try {
if (st != null) {
st.close();
}
if (conn != null) {
conn.close();
}
} catch(SQLException e) {
}
}
}
/**
* If a user disconnects in an unknown way that is never caught by onPlayerQuit,
* this will force close all records except for players currently online.
*/
public void catchUncaughtDisconnects(){
if (getConfig().getNode("joins", "enabled").getBoolean() ){
disconnectsTask = getGame().getScheduler().createTaskBuilder()
.async()
.delay(1L, TimeUnit.MINUTES)
.interval(1L, TimeUnit.MINUTES)
.execute(() -> {
String onUsers = "";
for(Player pl: getGame().getServer().getOnlinePlayers()) {
PluginPlayer pluginPlayer = PlayerIdentification.getOraclePlayer(pl);
if( pluginPlayer == null ) continue;
onUsers += pluginPlayer.getId()+",";
}
if(!onUsers.isEmpty()){
onUsers = onUsers.substring(0, onUsers.length()-1);
}
JoinUtil.forceDateForOfflinePlayers( onUsers );
}).submit(this);
}
}
/**
* If a user disconnects in an unknown way that is never caught by onPlayerQuit,
* this will force close all records except for players currently online.
*/
public void runAnnouncements(){
getGame().getScheduler().createTaskBuilder()
.async()
.delay(10L, TimeUnit.MINUTES)
.interval(10L, TimeUnit.MINUTES)
.execute(() -> {
List<Text> announces = AnnouncementUtil.getActiveAnnouncements();
if(!announces.isEmpty()){
if(lastAnnouncement >= announces.size()){
lastAnnouncement = 0;
}
game.getServer().getBroadcastChannel().send(announces.get(lastAnnouncement));
logger.info(announces.get(lastAnnouncement).toPlain());
lastAnnouncement++;
}
}).submit(this);
}
/**
* If a user disconnects in an unknown way that is never caught by onPlayerQuit,
* this will force close all records except for players currently online.
*/
public void runPlaytimeMonitor(){
if (getConfig().getNode("joins", "enabled").getBoolean()) {
getGame().getScheduler().createTaskBuilder()
.async()
.delay(12000L, TimeUnit.MILLISECONDS)
.interval(12000L, TimeUnit.MILLISECONDS)
.execute(new PlaytimeMonitor()).submit(this);
}
}
public void logDbError( SQLException e ){
logger.error("Database connection error: " + e.getMessage());
e.printStackTrace();
}
@Listener
public void onServerStop(GameStoppingServerEvent event) {
// Force offline date for everyone
if (getConfig().getNode("joins", "enabled").getBoolean()) {
JoinUtil.forceDateForAllPlayers();
}
disconnectsTask.cancel();
// Close pool connections when plugin disables
if (pool != null) {
pool.close();
}
}
}